import json
import os

from absl import flags

import coin.exchange.base.kr_rest.private_client_base as privcb
import coin.exchange.bithumb.kr_rest.native_private_client as nprivc
import coin.exchange.bithumb.kr_rest.product as exchange_product
from coin.exchange.bithumb.kr_rest.constants import coin_list, RETURN_CODE_SUCCESS
from coin.exchange.bithumb.kr_rest.currency import (to_native_currency, BithumbCurrency)

import coin.proto.coin_order_enums_pb2 as coin_order
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       CurrencyBalance,
                                       ProductFillElement,
                                       ProductFillBundle,
                                       ProductOrderElement,
                                       ProductOrderBundle)

FLAGS = flags.FLAGS

flags.DEFINE_string('bithumb_proxy_config', None, 'bithumb_proxy_config')


def to_float(num):
  if isinstance(num, str):
    return float(num.replace(',', ''))
  elif isinstance(num, (int, float)):
    return float(num)
  else:
    raise ValueError('Invalid input type %s', type(num))


# Extract currency from keys such as total_krw, in_use_krw.
def _extract_native_currency_from_key(key):
  currency = key.split('_')[-1]
  return currency


def _extract_native_currencies_from_json_msg(json_msg):
  return {_extract_native_currency_from_key(key) for key in json_msg['data']}


def _load_proxy_config(proxy_config_file):
  proxy = None
  if proxy_config_file is not None:
    assert os.path.exists(proxy_config_file), proxy_config_file
    with open(proxy_config_file) as f:
      proxy = json.loads(f.read())
  return proxy


class BithumbPrivateParser(object):
  @staticmethod
  def parse_balance(json_msg):
    # {
    #   'status':'0000',
    #   'data':{
    #     'total_krw':101000,
    #     'available_krw':101000,
    #     'xcoin_last':'21634000',
    #     'total_btc':'0.00000000',
    #     'misu_btc':'0.00000000',
    #     'in_use_krw':0,
    #     'in_use_btc':'0.00000000',
    #     'misu_krw':0,
    #     'available_btc':'0.00000000'
    #   }
    # }
    assert json_msg['status'] == '0000'
    native_currency_set = set([to_native_currency(currency).lower() for currency in coin_list]
                              + ['krw'])
    currency_set_in_json = _extract_native_currencies_from_json_msg(json_msg)
    currency_set = native_currency_set & currency_set_in_json  # Intersection
    currency_balance_list = []
    for native_currency in currency_set:
      currency = BithumbCurrency.FromStrNativeCurrencyNothrow(native_currency.upper())
      if currency is None:
        continue
      currency_balance = CurrencyBalance(
          currency=currency.currency,
          currency_native=currency.native_currency,
          total=to_float(json_msg['data']['total_' + native_currency]),
          available=to_float(json_msg['data']['available_' + native_currency]),
          hold=to_float(json_msg['data']['in_use_' + native_currency]))
      currency_balance_list.append(currency_balance)

    return AccountBalance(exchange='Bithumb', each_balance=currency_balance_list)

  @staticmethod
  def parse_fill(json_msg: dict, product) -> ProductFillBundle:
    """ Convert fill json to proto.
    search value:
    0: entire,
    1: purchase completed,
    2: sales completed,
    3: withdrawing,
    4: deposit,
    5: withdrawal,
    9: KRW depositing

    {
      "status": "0000",
      "data": [
        {
          "search": "2",
          "transfer_date": "1518395595034849",
          "order_currency": "BTC",
          "payment_currency": "KRW",
          "units": "- 4090.04",
          "price": "1139",
          "amount": "4658556",
          "fee": "0.00000000",
          "fee_currency": "KRW",
          "order_balance": "20490.59757800",
          "payment_balance": "72120642"
        },
        ......
        ......
        ......
      ]
    }
    """
    assert json_msg['status'] == '0000'
    update_msg_list = json_msg['data']

    currency_fill_list = []
    for update_msg in update_msg_list:
      if update_msg['search'] == '1':
        side = coin_order.BUY_ORDER
      elif update_msg['search'] == '2':
        side = coin_order.SELL_ORDER
      elif update_msg['search'] in ['0', '3', '4', '5', '9']:
        continue
      else:
        raise ValueError('Unexpected `search` value: %s\n' % update_msg)
      native_currency = product.base.native_currency.lower()
      # Note: fee value in json is always 0.0, sometimes there is a 'KRW'
      # postfix in the value string.
      currency_fill = ProductFillElement(symbol=product.symbol,
                                         side=side,
                                         price=to_float(update_msg['price']),
                                         qty=abs(to_float(update_msg['units'])),
                                         fee=to_float(update_msg['fee']),
                                         fill_timestamp=int(
                                             to_float(update_msg['transfer_date']) * 10**3))
      assert currency_fill.price > 0, currency_fill.price
      assert currency_fill.qty > 0, currency_fill.qty
      currency_fill_list.append(currency_fill)

    return ProductFillBundle(exchange='Bithumb',
                             symbol=product.symbol,
                             each_fill=currency_fill_list)

  @staticmethod
  def parse_orders(update_msg, product):
    """
    {
      "status": "0000",
      "data": [
        {
          "order_id": "1564992405793501",
          "order_currency": "BCH",
          "order_date": "1564992405793501",
          "payment_currency": "KRW",
          "type": "ask",
          "units": "10.0",
          "units_remaining": "10.0",
          "price": "405600"
        }
      ]
    }
    """
    currency_order_list = []
    for update_elem in update_msg['data']:
      currency_order = ProductOrderElement()
      currency_order.symbol = product.symbol
      currency_order.price = to_float(update_elem['price'])
      currency_order.qty = to_float(update_elem['units'])
      # no fee info
      currency_order.order_id = str(update_elem['order_id'])
      currency_order.exchange_order_id = str(update_elem['order_id'])
      currency_order.created_timestamp = int(to_float(update_elem['order_date']) * 1e3)
      if update_elem['type'] == 'ask':
        currency_order.side = coin_order.SELL_ORDER
      elif update_elem['type'] == 'bid':
        currency_order.side = coin_order.BUY_ORDER
      else:
        raise ValueError(update_elem['type'])
      currency_order_list.append(currency_order)

    return ProductOrderBundle(
        exchange='Bithumb',
        symbol=product.symbol,
        each_order=currency_order_list,
    )


class BithumbPrivateClient(privcb.PrivateClientBase):

  ProductType = exchange_product.BithumbProduct

  def __init__(self, key_file, proxy=None):
    if proxy is None:
      proxy = _load_proxy_config(FLAGS.bithumb_proxy_config)

    privcb.PrivateClientBase.__init__(self)
    self.nprivc = nprivc.BithumbNativePrivateClient(key_file=key_file, proxy=proxy)

  @staticmethod
  def _no_transaction_history(msg):
    no_transaction_history_msg = {'status': '5600', 'message': '거래 체결내역이 존재하지 않습니다.'}
    return msg == no_transaction_history_msg

  @staticmethod
  def _no_transaction_in_progress(msg):
    no_transaction_in_progress_msg = {'status': '5600', 'message': '거래 진행중인 내역이 존재하지 않습니다.'}
    return msg == no_transaction_in_progress_msg

  def query_account_balance_impl(self):
    # Currency can be ALL or single currency like BTC, ETH.
    currency = "ALL"
    update = self.nprivc.query_info_balance(currency)
    update.msg = BithumbPrivateParser.parse_balance(update.msg)
    return update

  def query_fills_impl(self, product):
    order_currency = product.base.native_currency
    payment_currency = product.quote.native_currency
    update = self.nprivc.query_info_user_transactions(order_currency=order_currency,
                                                      payment_currency=payment_currency)
    update.msg = BithumbPrivateParser.parse_fill(update.msg, product)
    return update

  def query_submit_impl(self, product, side, price, qty):
    order_currency = product.base.native_currency
    payment_currency = product.quote.native_currency
    assert side in ('buy', 'sell'), side
    side = 'bid' if side == 'buy' else 'ask'
    update = self.nprivc.query_trade_place(order_currency,
                                           payment_currency,
                                           price,
                                           qty,
                                           nprivc.BithumbOrderSide(side))
    return update

  def query_cancel_impl(self, product, order_id):
    # Don't know if it should be given `bid` or `ask`. Have to try both.
    try:
      update = self.nprivc.query_trade_cancel(product.base.native_currency,
                                              product.quote.native_currency,
                                              order_id,
                                              nprivc.BithumbOrderSide.BUY)
      if update.native_msg['status'] != RETURN_CODE_SUCCESS:
        update = self.nprivc.query_trade_cancel(product.base.native_currency,
                                                product.quote.native_currency,
                                                order_id,
                                                nprivc.BithumbOrderSide.SELL)

      if update.native_msg['status'] == RETURN_CODE_SUCCESS:
        return update
      else:
        # Return None means cancel failed.
        return None
    except Exception as ex:
      raise ex

  def query_cancel_product_impl(self, product):
    update = self.query_list_orders(product)
    for order in update.native_msg['data']:
      self.nprivc.query_trade_cancel(product.base.native_currency,
                                     product.quote.native_currency,
                                     order['order_id'],
                                     order['type'])

  def query_list_orders_impl(self, product):
    update = self.nprivc.query_info_orders(order_currency=product.base.native_currency,
                                           payment_currency=product.quote.native_currency,
                                           count=1000)
    if self._no_transaction_history(update.msg) or self._no_transaction_in_progress(update.msg):
      update.msg = {'status': '0000', 'data': []}
      update.native_msg = update.msg
    assert update.msg['status'] == RETURN_CODE_SUCCESS, update.msg
    update.msg = BithumbPrivateParser.parse_orders(update.native_msg, product)
    return update

  def query_account_list_orders_impl(self):
    raise NotImplementedError()

  def query_account_fills_impl(self):
    raise NotImplementedError()

  def query_account_position_impl(self):
    raise NotImplementedError()
